/*
 * @Descripttion:
 * @LastEditors: chen.long
 * @LastEditTime: 2024-02-24 14:55:19
 */
import { lifecycles } from '../types/index.type';
import { EventBus, EventObj, appEventObjMap } from '../lib/event';
import { plugin } from '../types/plugin.type';
import {
  SandboxCache,
  addSandboxCacheWithYbMicro,
  deleteYbMicroById,
  idToSandboxCacheMap
} from '../lib/sandboxCache';
import {
  appRouteParse,
  eventTrigger,
  getAbsolutePath,
  isFunction
} from '../utils';
import { ScriptResultList } from '../types/template.type';
import { syncUrlToIframe, syncUrlToWindow } from './sync';
import { rawDocumentQuerySelector } from '../constant/prototype';
import {
  patchEventTimeStamp,
  recoverEventListeners,
  insertScriptToIframe,
  iframeGenerator
} from './iframe';
import { getPlugins } from './plugin';
import {
  initRenderIframeAndContainer,
  clearChild,
  renderTemplateToIframe,
  removeLoading
} from './shadow';
import { localGenerator } from './proxy';

/**
 * @Author: chen.long
 * @Descripttion: 基于 Proxy和iframe 实现的沙箱
 */
export default class YbMicro {
  public id: string;
  // 激活时路由地址
  public url: string;
  // window代理
  public proxy: WindowProxy;
  // document代理
  public proxyDocument: Object;
  // location代理
  public proxyLocation: Object;
  // 事件中心
  public bus: EventBus;
  // 子应用显示容器
  public el: HTMLElement;
  // 子应用的template
  public template: string;
  // js沙箱
  public iframe: HTMLIFrameElement;
  // 支持子应用自定义fetch
  public fetch: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
  // 子应用的生命周期
  public lifecycles: lifecycles;
  // js沙箱ready态
  public iframeReady: Promise<void>;
  // 子应用js执行队列
  public execQueue: Array<Function>;
  // 标识子应用是否执行过
  public execFlag: boolean;
  // 子应用激活标志
  public activeFlag: boolean;
  // 子应用mount标志
  public mountFlag: boolean;
  // 子应用跳转标志
  public hrefFlag: boolean;
  // 子应用的插件
  public plugins: Array<plugin>;
  // 子应用document
  public document: Document;
  // 子应用head元素
  public head: HTMLHeadElement;
  // 子应用body元素
  public body: HTMLBodyElement;
  // 子应用dom监听事件留存，用于保存元素事件
  public elementEventCacheMap: WeakMap<
    Node,
    Array<{
      type: string;
      handler: EventListenerOrEventListenerObject;
      options: any;
    }>
  > = new WeakMap();

  // $ybmicro对象，提供给子应用的接口
  public provide: {
    bus: EventBus;
    props?: { [key: string]: any };
    location?: Object;
  };

  // 子应用嵌套场景，父应用传递给子应用的数据
  public inject: {
    idToSandboxMap: Map<String, SandboxCache>;
    appEventObjMap: Map<String, EventObj>;
    mainHostPath: string;
  };

  /**
   * @Author: chen.long
   * @Descripttion: 空闲时执行回调
   * @Param:
   * @param {*} callback 回调函数
   */
  public requestIdleCallback(callback) {
    return requestIdleCallback(() => {
      // 假如已经被销毁了
      if (!this.iframe) return;
      callback.apply(this);
    });
  }

  /**
   * @Author: chen.long
   * @Descripttion: 激活子应用
   * 1、同步路由
   * 2、动态修改iframe的fetch
   * 3、准备shadow
   * 4、准备子应用注入
   * @Param:
   */
  public async active(options: {
    url: string;
    template?: string;
    el?: string | HTMLElement;
    props?: { [key: string]: any };
  }): Promise<void> {
    const { url, el, props, template } = options;
    this.url = url;
    this.hrefFlag = false;
    this.provide.props = props ?? this.provide.props;
    this.activeFlag = true;
    // wait iframe init
    await this.iframeReady;

    // 处理子应用自定义fetch
    const iframeWindow = this.iframe.contentWindow;
    const iframeFetch = fetch
      ? (input: RequestInfo, init?: RequestInit) =>
          fetch(
            typeof input === 'string'
              ? getAbsolutePath(input, (this.proxyLocation as Location).href)
              : input,
            init
          )
      : this.fetch;
    if (iframeFetch) {
      iframeWindow.fetch = iframeFetch;
      this.fetch = iframeFetch;
    }

    // 处理子应用路由同步
    if (this.execFlag) {
      // 当保活模式下子应用重新激活时，只需要将子应用路径同步回主应用
      syncUrlToWindow(iframeWindow);
    } else {
      // 先将url同步回iframe，然后再同步回浏览器url
      syncUrlToIframe(iframeWindow);
      syncUrlToWindow(iframeWindow);
    }

    // inject template
    this.template = template ?? this.template;

    const iframeBody = rawDocumentQuerySelector.call(
      iframeWindow.document,
      'body'
    ) as HTMLElement;
    const { iframe, container } = initRenderIframeAndContainer(
      this.id,
      el ?? iframeBody
    );
    this.el = container;
    // 销毁js运行iframe容器内部dom
    if (el) clearChild(iframeBody);
    // 修复vue的event.timeStamp问题
    patchEventTimeStamp(iframe.contentWindow, iframeWindow);
    // 当销毁iframe时主动unmount子应用
    iframe.contentWindow.onunload = () => {
      this.unmount();
    };
    if (this.document) {
      iframe.contentDocument.replaceChild(
        this.document.documentElement,
        iframe.contentDocument.documentElement
      );
      // 保活场景需要事件全部恢复
      recoverEventListeners(
        iframe.contentDocument.documentElement,
        iframeWindow
      );
    } else {
      await renderTemplateToIframe(
        iframe.contentDocument,
        this.iframe.contentWindow,
        this.template
      );
    }
    this.document = iframe.contentDocument;
  }

  /**
   * @Author: chen.long
   * @Descripttion: 启动子应用
   * 1、运行js
   * 2、处理兼容样式
   */
  public async start(
    getExternalScripts: () => ScriptResultList
  ): Promise<void> {
    this.execFlag = true;
    // 执行脚本
    const scriptResultList = await getExternalScripts();
    // 假如已经被销毁了
    if (!this.iframe) return;
    const iframeWindow = this.iframe.contentWindow;
    // 标志位，执行代码前设置
    iframeWindow.__POWERED_BY_YBMICRO__ = true;
    // 同步代码
    const syncScriptResultList: ScriptResultList = [];
    // async代码无需保证顺序，所以不用放入执行队列
    const asyncScriptResultList: ScriptResultList = [];
    // defer代码需要保证顺序并且DOMContentLoaded前完成，这里统一放置同步脚本后执行
    const deferScriptResultList: ScriptResultList = [];
    scriptResultList.forEach(scriptResult => {
      if (scriptResult.defer) deferScriptResultList.push(scriptResult);
      else if (scriptResult.async) asyncScriptResultList.push(scriptResult);
      else syncScriptResultList.push(scriptResult);
    });

    // 同步代码
    syncScriptResultList.concat(deferScriptResultList).forEach(scriptResult => {
      this.execQueue.push(() =>
        scriptResult.contentPromise.then(content =>
          this.requestIdleCallback(() =>
            insertScriptToIframe({ ...scriptResult, content }, iframeWindow)
          )
        )
      );
    });

    // 异步代码
    asyncScriptResultList.forEach(scriptResult => {
      scriptResult.contentPromise.then(content => {
        this.requestIdleCallback(() =>
          insertScriptToIframe({ ...scriptResult, content }, iframeWindow)
        );
      });
    });

    //框架主动调用mount方法
    this.execQueue.push(() => this.requestIdleCallback(() => this.mount()));

    //触发 DOMContentLoaded 事件
    const domContentLoadedTrigger = () => {
      eventTrigger(iframeWindow.document, 'DOMContentLoaded');
      eventTrigger(iframeWindow, 'DOMContentLoaded');
      this.execQueue.shift()?.();
    };
    this.execQueue.push(() =>
      this.requestIdleCallback(domContentLoadedTrigger)
    );

    //触发 loaded 事件
    const domLoadedTrigger = () => {
      eventTrigger(iframeWindow.document, 'readystatechange');
      eventTrigger(iframeWindow, 'load');
      this.execQueue.shift()?.();
    };
    this.execQueue.push(() => this.requestIdleCallback(domLoadedTrigger));
    // 由于没有办法准确定位是哪个代码做了mount，提前关闭loading
    if (!isFunction(this.iframe.contentWindow.__YBMICRO_UNMOUNT))
      removeLoading(this.el);
    this.execQueue.shift()();

    // 所有的execQueue队列执行完毕，start才算结束，保证串行的执行子应用
    return new Promise(resolve => {
      this.execQueue.push(() => {
        resolve();
        this.execQueue.shift()?.();
      });
    });
  }

  /**
   * @Author: chen.long
   * @Descripttion: 框架主动发起mount，如果子应用是异步渲染实例，比如将生命周__YBMICRO_MOUNT放到async函数内
   * 此时采用fiber模式渲染（主应用调用mount的时机也是异步不确定的），框架调用mount时可能
   * 子应用的__YBMICRO_MOUNT还没有挂载到window，所以这里封装一个mount函数，当子应用是异步渲染
   * 实例时，子应用异步函数里面最后加上window.__YBMICRO.mount()来主动调用
   */
  public mount(): void {
    if (this.mountFlag) return;
    if (isFunction(this.iframe.contentWindow.__YBMICRO_MOUNT)) {
      removeLoading(this.el);
      this.lifecycles?.beforeMount?.(this.iframe.contentWindow);
      this.iframe.contentWindow.__YBMICRO_MOUNT();
      this.lifecycles?.afterMount?.(this.iframe.contentWindow);
      this.mountFlag = true;
    }
    this.lifecycles?.activated?.(this.iframe.contentWindow);
    this.execQueue.shift()?.();
  }

  /**
   * @Author: chen.long
   * @Descripttion: 保活模式和使用proxyLocation.href跳转链接都不应该销毁shadow
   */
  public unmount(): void {
    this.activeFlag = false;
    this.lifecycles?.deactivated?.(this.iframe.contentWindow);
    if (!this.mountFlag) return;
  }

  /**
   * @Author: chen.long
   * @Descripttion: 销毁子应用
   */
  public destroy() {
    this.unmount();
    this.bus.$clear();
    this.proxy = null;
    this.proxyDocument = null;
    this.proxyLocation = null;
    this.execQueue = null;
    this.provide = null;
    this.bus = null;
    this.execFlag = null;
    this.mountFlag = null;
    this.hrefFlag = null;
    this.document = null;
    this.head = null;
    this.body = null;
    this.elementEventCacheMap = null;
    this.lifecycles = null;
    this.provide = null;
    this.inject = null;
    this.execQueue = null;
    this.plugins = null;
    // 清除 dom
    if (this.el) {
      clearChild(this.el);
      this.el = null;
    }
    // 清除 iframe 沙箱
    if (this.iframe) {
      const iframeWindow = this.iframe.contentWindow;
      if (iframeWindow?.__YBMICRO_EVENTLISTENER__) {
        iframeWindow.__YBMICRO_EVENTLISTENER__.forEach(o => {
          iframeWindow.removeEventListener(o.type, o.listener, o.options);
        });
      }
      this.iframe.parentNode?.removeChild(this.iframe);
      this.iframe = null;
    }
    deleteYbMicroById(this.id);
  }

  /**
   * @Author: chen.long
   * @Descripttion: 构造函数
   * @Param:
   * @param id 子应用的id，唯一标识
   * @param url 子应用的url，可以包含protocol、host、path、query、hash
   */
  constructor(options: { name: string; url: string; lifecycles: lifecycles }) {
    // 传递inject给嵌套子应用
    if (window.__POWERED_BY_YBMICRO__)
      this.inject = window.__YBMICRO.inject as any;
    else {
      this.inject = {
        idToSandboxMap: idToSandboxCacheMap,
        appEventObjMap,
        mainHostPath: window.location.protocol + '//' + window.location.host
      };
    }
    const { name, url, lifecycles } = options;
    this.id = name;
    this.bus = new EventBus(this.id);
    this.url = url;
    this.provide = { bus: this.bus };
    this.execQueue = [];
    this.lifecycles = lifecycles;
    this.plugins = getPlugins();

    // 创建目标地址的解析
    const { urlElement, appHostPath, appRoutePath } = appRouteParse(url);
    const { mainHostPath } = this.inject;
    // 创建iframe
    this.iframe = iframeGenerator(
      this,
      mainHostPath,
      appHostPath,
      appRoutePath
    );

    const { proxyDocument, proxyLocation } = localGenerator(
      this.iframe,
      urlElement,
      mainHostPath,
      appHostPath
    );
    this.proxyDocument = proxyDocument;
    this.proxyLocation = proxyLocation;
    this.provide.location = this.proxyLocation;

    addSandboxCacheWithYbMicro(this.id, this);
  }
}
